from django import template
from django.contrib import admin
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import render_to_response
from django.utils.html import escape
from django.utils.translation import ugettext as _
from django.utils.encoding import force_unicode
import models


class QuestionInline(admin.TabularInline):
    model = models.Question
    max_num = settings.OPINION_EXTRACTOR['max_questions_per_questionnaire']


class QuestionnaireAdmin(admin.ModelAdmin):
    date_hierarchy = 'published_on'
    list_display = (
        'title', 'created_by', 'created_on', 'publish_status', 'published_on',
        'min_sample_size'
    )
    list_filter = (
        'created_on', 'publish_status', 'published_on'
    )
    inlines = (QuestionInline,)
    prepopulated_fields = { 'slug': ('title',) }
    save_on_top = True
    # TODO: Add back the description field in the list of search fields
    search_fields = ('title', 'slug') #, 'description')
    
    _min_questions = settings.OPINION_EXTRACTOR['min_questions_per_questionnaire']
    
    def __call__(self, request, url):
        url_patterns = (
            ('/preview', self.preview_view),
            ('/dashboard', self.dashboard_view),
            ('/dashboard/csv-export', self.csv_export),
        )
        if url is not None:
            for url_pattern, view in url_patterns:
                if url.endswith(url_pattern):
                    return view(request, admin.util.unquote(
                        url[:-len(url_pattern)]
                    ))
        return super(QuestionnaireAdmin, self).__call__(request, url)
        
    def _can_change_object_state(self, request, obj=None):
        """
        Returns True if the given request has permission to change the given
        Questionnaire state.

        Only the Questionnaires with publish_status set to 'Draft' are allowed
        to change.
        """
        if not obj:
            return True # So the user can see the change list page
        return (request.user.is_superuser or obj.created_by == request.user) \
            and obj.publish_status == 'D'
    
    def has_change_permission(self, request, obj=None):
        return super(QuestionnaireAdmin, self).has_change_permission(request, obj) and \
            self._can_change_object_state(request, obj)
    
    def has_delete_permission(self, request, obj=None):
        return super(QuestionnaireAdmin, self).has_delete_permission(request, obj) and \
            self._can_change_object_state(request, obj)
    
    def queryset(self, request):
        qs = super(QuestionnaireAdmin, self).queryset(request)
        if request.user.is_superuser:
            return qs
        else:
            return qs.filter(created_by=request.user)

    def save_model(self, request, obj, form, change):
        obj.created_by = request.user
        obj.save()

    def _post_relationships_changed(self, obj):
        """
        A hook that is called after all relationships to the object have been
        changed (via ``self.save_m2m`` and ``self.save_formset``)
        
        Could be done as a post-save signal in ``Question model`` but it would
        generate multiple ``Questionnaire`` saves (1 for each ``Question``)
        """
        obj.num_questions = obj.questions.count()
        obj.save()

    def response_add(self, request, obj, post_url_continue='../%s/'):
        self._post_relationships_changed(obj)
        return super(QuestionnaireAdmin, self).response_add(
            request,
            obj,
            post_url_continue='../%s/'
        )

    def response_change(self, request, obj):
        self._post_relationships_changed(obj)
        return super(QuestionnaireAdmin, self).response_change(request, obj)

    def _get_obj(self, request, object_id, permission_test_func=None):
        """
        Returns the object of this model or raises ``Http404`` exception if
        there is no object with that ``object_id``

        If ``permission_test_func`` is set and its call returns False
        ``PermissionDenied`` exception will be raised.
        """
        model = self.model
        opts = model._meta
        try:
            cache_key = "questionnaire::%s" % object_id
            obj = cache.get(cache_key)
            if obj is None:
                obj = self.queryset(request).get(pk=object_id)
                if obj.publish_status != 'D':
                    cache.set(cache_key, obj)
        except model.DoesNotExist:
            # Don't raise Http404 just yet, because we haven't checked
            # permissions yet. We don't want an unauthenticated user to be able
            # to determine whether a given object exists.
            obj = None

        if permission_test_func and not permission_test_func(request, obj):
            raise PermissionDenied

        if obj is None:
            raise Http404(_("%s object with primary key %r does not exist.") % (
                force_unicode(opts.verbose_name),
                escape(object_id)
            ))
        else:
            return obj

    def log_publish_status_change(self, request, object):
        self.log_change(request, object, _('Changed publish_status to "%s".') %
            force_unicode(object.get_publish_status_display())
        )
        
    def change_view(self, request, object_id, extra_context=None):
        try:
            obj = self._get_obj(request, object_id, None)
            # This hack is needed because most URLs are hardcoded in the admin.
            # Questionnaires with publish_status different from 'Draft' are
            # redirected to the dashboard view (bacuse they can't be changed)
            if obj.publish_status != 'D':
                return HttpResponseRedirect("./dashboard/")
        except Http404:
            pass # Just fallback to ModelAdmin.change_view
        return super(QuestionnaireAdmin, self).change_view(
            request,
            object_id,
            extra_context=extra_context
        )

    @transaction.commit_on_success
    def preview_view(self, request, object_id, extra_context=None):
        obj = self._get_obj(request, object_id, self.has_change_permission)
        opts = self.model._meta
        num_questions = obj.questions.count()
        
        if request.POST: # The user wants to publish the questionnaire
            if num_questions < self._min_questions: # Sanity check
                raise PermissionDenied
            
            # OK, safe to publish
            from datetime import datetime
            obj.publish_status = 'L'
            obj.published_on = datetime.now()
            obj.save()
            
            # Log the status change, message the user and redirect to dashboard
            self.log_publish_status_change(request, obj)
            self.message_user(request, _('The %(name)s "%(obj)s" was published successfully.') % {
                'name': force_unicode(opts.verbose_name),
                'obj': force_unicode(obj)
            })
            return HttpResponseRedirect("../dashboard/")
        else: # Show the preview
            context = {
                'title': _("Preview %s") % force_unicode(opts.verbose_name),
                'object_name': force_unicode(opts.verbose_name),
                'object': obj,
                'questions': obj.questions.all(),
                'num_questions': num_questions,
                'min_questions': self._min_questions,
                'can_publish': num_questions >= self._min_questions,
                'opts': opts,
                'root_path': self.admin_site.root_path,
                'app_label': opts.app_label,
            }
            context.update(extra_context or {})
            # TODO: Prettify the preview.html template
            return render_to_response(
                "admin/%s/%s/preview.html" % (
                    opts.app_label,
                    opts.object_name.lower()
                ),
                context,
                context_instance=template.RequestContext(request)
            )

    def dashboard_view(self, request, object_id, extra_context=None):
        obj = self._get_obj(request, object_id, None)
        opts = self.model._meta
        
        # Questionnaires in 'Draft' publish_status are redirected to change view
        if obj.publish_status == 'D':
            return HttpResponseRedirect("../")
        
        # Agregate results
        from datetime import date
        from django.db.models import Count
        from opinion_extractor.opinion.models import Opinion

        cache_key = "questions::%s" % obj.pk
        questions = cache.get(cache_key)
        if questions is None:
            questions = obj.questions.all()
            cache.set(cache_key, questions)
        all_answers = [q.answers for q in questions]
        num_questions = len(questions)

        valid_on = date.today()

        query = Opinion._default_manager.filter(questionnaire=obj).valid_on_date(valid_on)

        cache_key = "num_opinions::%s::%s" % (obj.pk, valid_on)
        num_opinions = cache.get(cache_key)
        if num_opinions is None:
            num_opinions = query.count()
            cache.set(cache_key, num_opinions)


        cache_key = "questions_spec::%s::%s" % (obj.pk, valid_on)
        questions_spec = cache.get(cache_key)
        if questions_spec is None:
            questions_spec = []
            if num_opinions > 0:
                for i, answer_field in enumerate(Opinion._meta.answer_fields[:num_questions]):
                    question = questions[i]
                    # Prefill all opinion counts for every anser to 0 and then
                    # insert real number of opinions where > 0
                    counts = dict.fromkeys(range(question.num_answers), 0)
                    counts.update(map(
                        lambda x: (x[answer_field], x['answers']),
                        query.values(answer_field).annotate(
                            answers=Count(answer_field)
                        ).order_by(answer_field)
                    ))
                    opinions= map(
                        lambda (key, count): dict(
                            title=question.answers[key],
                            count=count
                        ),
                        counts.items()
                    )
                    questions_spec.append(dict(
                        title=question.title,
                        opinions=opinions,
                    ))
            cache.set(cache_key, questions_spec)

        
        context = {
            'title': _('Dashboard for the "%s" questionnaire') % obj,
            'object_name': force_unicode(opts.verbose_name),
            'object': obj,
            'questions': questions_spec,
            'num_opinions': num_opinions,
            'can_download_opinions_report': num_opinions >= obj.min_sample_size,
            'opts': opts,
            'root_path': self.admin_site.root_path,
            'app_label': opts.app_label,
        }
        context.update(extra_context or {})
        # TODO: Prettify the dashboard.html template
        return render_to_response(
            "admin/%s/%s/dashboard.html" % (
                opts.app_label,
                opts.object_name.lower()
            ),
            context,
            context_instance=template.RequestContext(request)
        )
    
    def csv_export(self, request, object_id, extra_context=None):
        from datetime import date
        from django.db.models import TimeField
        from django.template.defaultfilters import capfirst
        from opinion_extractor.opinion.models import Opinion
        from opinion_extractor.utils import unicode_csv
        questionnaire = self._get_obj(request, object_id, None)
        
        # Questionnaires in 'Draft' publish_status are redirected to change view
        if questionnaire.publish_status == 'D':
            return HttpResponseRedirect("../../")
        
        valid_on = date.today()
        
        opinions = Opinion._default_manager.valid_on_date(valid_on).filter(
            questionnaire=questionnaire
        )
        # Can download opinions report only if there are enough opinions
        if len(opinions) < questionnaire.min_sample_size:
            raise PermissionDenied
        
        questions = questionnaire.questions.all()
        answers = [q.answers for q in questions]
        
        def _get_field_info(field_name, field_verbose_name=None, value_func=None):
            field = Opinion._meta.get_field(field_name)
            if field_verbose_name is None:
                field_verbose_name = capfirst(force_unicode(field.verbose_name))
            if value_func is None:
                if len(field.choices) > 0:
                    value_func = lambda val: force_unicode(dict(field.choices)[val])
                elif isinstance(field, TimeField): # Excel doesn't like microseconds
                    value_func = lambda val: val.replace(microsecond=0)
                else:
                    value_func = lambda val: val
            return {
                'field_name': field_name,
                'field_verbose_name': field_verbose_name,
                'value_func': value_func
            }
        
        def _get_fields_for_report():
            fields = []
            meta = Opinion._meta
            for i, field_name in enumerate(meta.answer_fields[:questionnaire.num_questions]):
                def _wrapper(ans):
                    return lambda x: ans[x]
                fields.append(_get_field_info(
                    field_name,
                    force_unicode(questions[i]),
                    _wrapper(answers[i])
                ))
            other_field_names = [
                'num_failled_attempts', 'valid_from', 'responded_at_time',
                'response_time_sec'
            ]
            other_field_names.extend(meta.respondent_fields)
            for field_name in other_field_names:
                fields.append(_get_field_info(field_name))
            return fields
        fields_for_report = _get_fields_for_report()
        
        def _get_row_header():
            return map(lambda x: x['field_verbose_name'], fields_for_report)
        
        def _get_row_data(opinion):
            return map(lambda x: x['value_func']( getattr(opinion, x['field_name']) ), fields_for_report)
        
        buffer = [_get_row_header()]
        for o in opinions:
            buffer.append(_get_row_data(o))
        
        response = HttpResponse(mimetype='text/csv')
        response['Content-Disposition'] = 'attachment; filename=%s-export.csv' % questionnaire.slug
        writer = unicode_csv.UnicodeWriter(response)
        writer.writerows(buffer)
        return response


admin.site.register(models.Questionnaire, QuestionnaireAdmin)
